
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_javascript.h"

# ifdef NGX_JAVASCRIPT_HAVE_MYSQL
#include <mysql.h>
static char *ngx_mysql_groups[] = {
	"client",
	"javascript",
	(char *) NULL
};
# endif /* NGX_JAVASCRIPT_HAVE_MYSQL */

/*
 * Usage
 */
int ngx_javascript_main_usage(ngx_javascript_server_t *server) {
	fprintf(stderr,
"Usage: %s [options]\n"
"Options:\n"
" -h, --help, --usage            This help message\n"
" -x, --foreground               Run in foreground. Default: false\n"
" -p, --pid <pidfile>            Write PID to <pidfile>. Default: " NGX_JAVASCRIPT_PREFIX "/logs/javascript.pid\n"
" -l, --log <logfile>            Write logs to <logfile>. Default: " NGX_JAVASCRIPT_PREFIX "/logs/javascript.log\n"
" -t, --temp <tempdir>           Write files to <tempdir>. Default: " NGX_JAVASCRIPT_PREFIX "/temp\n"
" -s, --socket <socket>          Listen unix domain <socket>. Default: " NGX_JAVASCRIPT_PREFIX "/temp/javascript.sock\n"
" -c, --count <count>            Number of simultaneous clients. Default: 100\n"
" -m, --mode <mode>              Set socket mode to <mode>. Default: 0600\n"
" -u, --uid <uid>                Set UID <uid>. Default: none\n"
" -g, --gid <gid>                Set GID <gid>. Default: none\n"
"     --max-exec <time>          Max JavaScript execution <time> sec. Default: 10\n"
"     --max-runtime <size>       Max JavaScript memory before GC. Default: 100M\n"
"     --max-stack <size>         Max JavaScript stack size. Default: 8M\n"
"     --max-form <size>          Max post data <size>. Default: 1M\n"
"       application/x-www-form-urlencoded or multipart/form-data\n"
"       is read info memory and parsed on demand\n"
"     --max-data <size>          Max upload data <size>. Default: 2M\n"
"       multipart/form-data\n"
"       is written to file and parsed on demand\n"
"", server->program
	);
	return -1;
}

/*
 * Parse options
 */
int ngx_javascript_main_options(ngx_javascript_server_t *server, int argc, char *argv[]) {
	int i;
	server->program = argv[0];
	for (i = 1; i < argc; i ++) {
		char *o = argv[i];
		int l = strlen(o);
		if (o[0] != '-') {
			ngx_javascript_log(0, "%s: unknown option: %s\n", server->program, o);
			return -1;
		}
		if ((l == 2 && o[1] == 'h') || (l == sizeof("--help") - 1 && strcmp(o, "--help") == 0) || (l == sizeof("--usage") - 1 && strcmp(o, "--usage") == 0)) {
			return ngx_javascript_main_usage(server);
		} else if ((l == 2 && o[1] == 'x') || (l == sizeof("--foreground") - 1 && strcmp(o, "--foreground") == 0)) {
			server->fg = 1;
			continue;
		} else if ((l == 2 && o[1] == 'p') || (l == sizeof("--pid") - 1 && strcmp(o, "--pid") == 0)) {
			if (i + 1 < argc) {
				server->pid_file = argv[++ i];
				continue;
			}
		} else if ((l == 2 && o[1] == 'l') || (l == sizeof("--log") - 1 && strcmp(o, "--log") == 0)) {
			if (i + 1 < argc) {
				server->log_file = argv[++ i];
				continue;
			}
		} else if ((l == 2 && o[1] == 't') || (l == sizeof("--temp") - 1 && strcmp(o, "--temp") == 0)) {
			if (i + 1 < argc) {
				server->temp_path = argv[++ i];
				continue;
			}
		} else if ((l == 2 && o[1] == 's') || (l == sizeof("--socket") - 1 && strcmp(o, "--socket") == 0)) {
			if (i + 1 < argc) {
				server->socket_path = argv[++ i];
				continue;
			}
		} else if ((l == 2 && o[1] == 'c') || (l == sizeof("--count") - 1 && strcmp(o, "--count") == 0)) {
			if (i + 1 < argc) {
				i ++;
				int n = ngx_javascript_atoi(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->clients_max = n;
					server->workers_max = n;
				} else {
					ngx_javascript_log(0, "%s: option %s requires integer number, not \"%s\"\n", server->program, o, argv[i]);
					return -1;
				}
				continue;
			}
		} else if ((l == 2 && o[1] == 'm') || (l == sizeof("--mode") - 1 && strcmp(o, "--mode") == 0)) {
			if (i + 1 < argc) {
				i ++;
				int n = ngx_javascript_atoo(argv[i], strlen(argv[i])) & 0777;
				if (n > 0) {
					server->socket_mode = n;
				} else {
					ngx_javascript_log(0, "%s: option %s requires octal number, not \"%s\"\n", server->program, o, argv[i]);
					return -1;
				}
				continue;
			}
		} else if ((l == 2 && o[1] == 'u') || (l == sizeof("--uid") - 1 && strcmp(o, "--uid") == 0)) {
			if (i + 1 < argc) {
				i ++;
				int n = ngx_javascript_atoi(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->uid = n;
				} else if (n < 0) {
					struct passwd *p = getpwnam(argv[i]);
					if (p != NULL) {
						server->uid = p->pw_uid;
						if (server->gid == 0) {
							server->gid = p->pw_gid;
						}
					} else {
						ngx_javascript_log(0, "%s: option %s: %s\n", server->program, o, errno == 0 ? "User not found" : strerror(errno));
						return -1;
					}
				}
				continue;
			}
		} else if ((l == 2 && o[1] == 'g') || (l == sizeof("--gid") - 1 && strcmp(o, "--gid") == 0)) {
			if (i + 1 < argc) {
				i ++;
				int n = ngx_javascript_atoi(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->gid = n;
				} else if (n < 0) {
					struct group *p = getgrnam(argv[i]);
					if (p != NULL) {
						server->gid = p->gr_gid;
					} else {
						ngx_javascript_log(0, "%s: option %s: %s: %s\n", server->program, o, argv[i], errno == 0 ? "Group not found" : strerror(errno));
						return -1;
					}
				}
				continue;
			}
		} else if (l == sizeof("--max-exec") - 1 && strcmp(o, "--max-exec") == 0) {
			if (i + 1 < argc) {
				i ++;
				size_t n = ngx_javascript_atoi(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->timeout = n;
				} else {
					ngx_javascript_log(0, "%s: option %s requires integer number, not \"%s\"\n", server->program, o, argv[i]);
					return -1;
				}
				continue;
			}
		} else if (l == sizeof("--max-runtime") - 1 && strcmp(o, "--max-runtime") == 0) {
			if (i + 1 < argc) {
				i ++;
				size_t n = ngx_javascript_parse_size(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->max_runtime = n;
				} else {
					ngx_javascript_log(0, "%s: option %s requires size, not \"%s\"\n", server->program, o, argv[i]);
					return -1;
				}
				continue;
			}
		} else if (l == sizeof("--max-stack") - 1 && strcmp(o, "--max-stack") == 0) {
			if (i + 1 < argc) {
				i ++;
				size_t n = ngx_javascript_parse_size(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->max_stack = n;
				} else {
					ngx_javascript_log(0, "%s: option %s requires size, not \"%s\"\n", server->program, o, argv[i]);
					return -1;
				}
				continue;
			}
		} else if (l == sizeof("--max-form") - 1 && strcmp(o, "--max-form") == 0) {
			if (i + 1 < argc) {
				i ++;
				size_t n = ngx_javascript_parse_size(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->max_form = n;
				} else {
					ngx_javascript_log(0, "%s: option %s requires size, not \"%s\"\n", server->program, o, argv[i]);
					return -1;
				}
				continue;
			}
		} else if (l == sizeof("--max-data") - 1 && strcmp(o, "--max-data") == 0) {
			if (i + 1 < argc) {
				i ++;
				size_t n = ngx_javascript_parse_size(argv[i], strlen(argv[i]));
				if (n > 0) {
					server->max_data = n;
				} else {
					ngx_javascript_log(0, "%s: option %s requires size, not \"%s\"\n", server->program, o, argv[i]);
					return -1;
				}
				continue;
			}
		} else {
			ngx_javascript_log(0, "%s: unknown option: %s\n", server->program, o);
			return -1;
		}
		ngx_javascript_log(0, "%s: option %s requires an argument\n", server->program, argv[i]);
		return -1;
	}
	return 0;
}

/*
 * Check if JavaScript server is running. Kill it. Return -1 on error, 0 to exit, 1 to start
 */
int ngx_javascript_main_running(ngx_javascript_server_t *server) {
	int i;
	pid_t pid = -1;
	FILE *F = fopen(server->pid_file, "r");
	if (F == NULL) return 1;
	fscanf(F, "%d", &pid);
	fclose(F);
	if (pid < 0) return 1;
	if (kill(pid, 0) < 0) return 1;
	
	if (kill(pid, SIGINT) < 0) return -1;
	for (i = 0; i < 1000; i ++) {
		if (kill(pid, 0) < 0) return 1;
		usleep(1000);
	}
	if (kill(pid, SIGTERM) < 0) return -1;
	if (kill(pid, 0) < 0) return 1;
	return 0;
}

/*
 * Daemonize and write pid to file
 */
int ngx_javascript_main_daemonize(ngx_javascript_server_t *server) {
	pid_t pid;
	if (server->fg) return 1;
	pid = fork();
	if (pid < 0) {
		ngx_javascript_log(0, "%s: fork(): %s\n", server->program, strerror(errno));
		return -1;
	} else if (pid > 0) {
		// This is parent. It stores pid and exits
		FILE *F = fopen(server->pid_file, "w");
		if (F == NULL) {
			ngx_javascript_log(0, "%s: %s: %s\n", server->program, server->pid_file, strerror(errno));
			return -1;
		}
		fprintf(F, "%d\n", pid);
		fclose(F);
		close(server->fd);
		return 0;
	}
	// This is child
	fclose(stdin);
	fclose(stdout);
	fclose(stderr);
	setsid();
	setpgrp();
	stderr = fopen(server->log_file, "a");
	server->pid = getpid();
	return 1;
}

// This is declared global for watchdog signal handler
static pid_t server_pid;

/*
 * Signal received. What to do?
 */
void ngx_javascript_main_signal(int signal) {
	kill(server_pid, signal);
}

/*
 * Start watchdog - a process witch monitors the server and starts a new one
 */
int ngx_javascript_main_start(ngx_javascript_server_t *server) {
	pid_t pid;
	int status;
	if (server->fg) return ngx_javascript_main_run(server);
	while (1) {
		pid = fork();
		if (pid < 0) {
			ngx_javascript_log(0, "%s: fork(): %s\n", server->program, strerror(errno));
			return -1;
		} else if (pid > 0) {
			server_pid = pid;
			signal(SIGINT, ngx_javascript_main_signal);
			// This is parent
			while (waitpid(pid, &status, 0) > 0) {
				if (WIFEXITED(status) || WIFSIGNALED(status)) break;
			}
			// We are here if child has exited or is terminated
		} else {
			// This is child. Start main loop
			ngx_javascript_main_run(server);
		}
	}
}

/*
 * Start main loop with all needed memory allocations
 */
int ngx_javascript_main_run(ngx_javascript_server_t *server) {
	server->clients = ngx_javascript_calloc(server->clients_max * sizeof(ngx_javascript_client_t *));
	server->workers = ngx_javascript_calloc(server->workers_max * sizeof(ngx_javascript_worker_t *));
	
	if (server->clients == NULL || server->workers == NULL) {
		ngx_javascript_log(0, "%s: %s\n", server->program, strerror(errno));
		return 1;
	}
	
# ifdef NGX_JAVASCRIPT_HAVE_MYSQL
	mysql_library_init(0, NULL, ngx_mysql_groups);
# endif /* NGX_JAVASCRIPT_HAVE_MYSQL */
	
	JS_SetCStringsAreUTF8();
	server->jsr = JS_NewRuntime(server->max_runtime);
	
	server->loop = ev_default_loop(0);
	
	server->event.server = server;
	ev_set_userdata(server->loop, server);
	
	ev_init(&server->event.io, ngx_javascript_server_accept_event);
	ev_io_set(&server->event.io, server->fd, EV_READ);
	ev_io_start(server->loop, &server->event.io);
	
	ev_init(&server->sigint_event, ngx_javascript_server_signal_event);
	ev_signal_set(&server->sigint_event, SIGINT);
	ev_signal_start(server->loop, &server->sigint_event);
	
	ev_init(&server->async_event, ngx_javascript_server_async_event);
	ev_async_start(server->loop, &server->async_event);
	
	ev_loop(server->loop, 0);
	
	return 0;
}

/*
 * Main
 */
int main(int argc, char *argv[]) {
	int r;
	ngx_javascript_server_t server_s;
	ngx_javascript_server_t *server = &server_s;
	memset(server, 0, sizeof(ngx_javascript_server_t));
	
	// Default options
	
	server->clients_max = 100;
	server->workers_max = 100;
	
	server->max_form = 1024 * 1024;
	server->max_data = 2 * 1024 * 1024;
	
	server->max_stack = 8 * 1024 * 1024;
	server->max_runtime = 100L * 1024L * 1024L;
	
	server->socket_mode = 0600;
	
	server->timeout = 10;
	
	server->pid_file = NGX_JAVASCRIPT_PREFIX "/logs/javascript.pid";
	server->log_file = NGX_JAVASCRIPT_PREFIX "/logs/javascript.log";
	server->temp_path = NGX_JAVASCRIPT_PREFIX "/temp";
	server->socket_path = NGX_JAVASCRIPT_PREFIX "/temp/javascript.sock";
	
	server->pagesize = getpagesize();
	
	// Maybe somebody wants global lock?
	pthread_mutex_init(&server->lock, (pthread_mutexattr_t *) NULL);
	
	// Parse options
	if (ngx_javascript_main_options(server, argc, argv) < 0) return 1;
	
	// Is server running? If it is, we kill it and start a new one
	r = ngx_javascript_main_running(server);
	if (r < 0) return 1;
	if (r == 0) return 0;
	
	// Create a listening socket
	int fd = ngx_javascript_server_socket(server);
	if (fd < 0) {
		ngx_javascript_log(0, "%s: %s: %s\n", server->program, server->socket_path, strerror(errno));
		return 1;
	}
	server->fd = fd;
	
	// We daemonize after creating socket to print socket errors to terminal and not to logfile
	r = ngx_javascript_main_daemonize(server);
	if (r < 0) return 1;
	if (r == 0) return 0;
	
	// We do setuid() and setgid() after creating socket just for the case socket directory is not writable
	if (getgid() == 0 && server->gid > 0) setgid(server->gid);
	if (getuid() == 0 && server->uid > 0) setuid(server->uid);
	
	// Start a watchdog which then starts main loop
	return ngx_javascript_main_start(server);
}

int ngx_javascript_atoi(char *line, size_t n) {
	int value;
	if (n == 0) return -1;
	for (value = 0; n --; line ++) {
		if (*line < '0' || *line > '9') return -1;
		value = value * 10 + (*line - '0');
	}
	return value;
}

int ngx_javascript_atoo(char *line, size_t n) {
	int value;
	if (n == 0) return -1;
	for (value = 0; n --; line ++) {
		if (*line < '0' || *line > '7') return -1;
		value = value * 8 + (*line - '0');
	}
	return value;
}

size_t ngx_javascript_atos(char *line, size_t n) {
	int value;
	if (n == 0) return 0;
	for (value = 0; n --; line ++) {
		if (*line < '0' || *line > '9') return 0;
		value = value * 10 + (*line - '0');
	}
	return value;
}

size_t ngx_javascript_parse_size(char *line, size_t n) {
	char unit;
	size_t len;
	ssize_t size;
	int scale;
	
	len = n;
	unit = line[len - 1];
	switch (unit) {
		case 'K':
		case 'k':
			len --;
			scale = 1024;
			break;
		case 'M':
		case 'm':
			len--;
			scale = 1024 * 1024;
			break;
		default:
			scale = 1;
	}
	size = ngx_javascript_atos(line, len);
	size *= scale;
	return size;
}

void *ngx_javascript_calloc(size_t size) {
	return calloc(size, 1);
}

void *ngx_javascript_malloc(size_t size) {
	void *r = malloc(size);
	if (r == NULL) {
		ngx_javascript_log(0, "malloc(): %s\n", strerror(errno));
	}
	return r;
}

void ngx_javascript_free(void *ptr) {
	return free(ptr);
}

void *ngx_javascript_realloc(void *ptr, size_t size) {
	return realloc(ptr, size);
}

void *JS_calloc(JSContext *jcx, size_t size) {
	void *r = JS_malloc(jcx, size);
	if (r == NULL) {
		ngx_javascript_log(0, "malloc(): %s\n", strerror(errno));
	} else {
		memset(r, 0, size);
	}
	return r;
}

void ngx_javascript_log(int level, const char *fmt, ...) {
	va_list ap;
	va_start(ap, fmt);
	ngx_javascript_log_va(fmt, ap);
	va_end(ap);
}

void ngx_javascript_log_va(const char *fmt, va_list ap) {
	FILE *F = stderr;
	vfprintf(F, fmt, ap);
	fflush(F);
}



void ngx_javascript_timer(int report) {
	struct timeval tv;
	struct timezone tz;
	static struct timeval ngx_javascript_tv;
	static struct timezone ngx_javascript_tz;
	if (gettimeofday(&tv, &tz) == 0) {
		if (report)
			fprintf(stderr, "%.6f sec\n", 0.000001 * (tv.tv_usec - ngx_javascript_tv.tv_usec) + (tv.tv_sec - ngx_javascript_tv.tv_sec));
		ngx_javascript_tv = tv;
		ngx_javascript_tz = tz;
	}
}
